package drds.plus.sequence.impl;

import drds.plus.common.lifecycle.AbstractLifecycle;
import drds.plus.sequence.Range;
import drds.plus.sequence.SequenceDao;
import drds.plus.sequence.SequenceException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;

import javax.sql.DataSource;
import java.sql.*;

@Data
@Slf4j
public class SequenceDaoImpl extends AbstractLifecycle implements SequenceDao {

    private static final int MIN_STEP = 1;
    private static final int MAX_STEP = 100000;
    private static final int DEFAULT_STEP = 1000;
    private static final int DEFAULT_RETRY_TIMES = 150;

    private static final String DEFAULT_TABLE_NAME = "sequence";
    private static final String DEFAULT_NAME_COLUMN_NAME = "id";
    private static final String DEFAULT_VALUE_COLUMN_NAME = "value";
    private static final String DEFAULT_GMT_MODIFIED_COLUMN_NAME = "gmt_modified";

    private static final long DELTA = 100000000L;

    private DataSource dataSource;

    /**
     * 重试次数
     */
    private int retryTimes = DEFAULT_RETRY_TIMES;

    /**
     * 步长
     */
    private int step = DEFAULT_STEP;

    /**
     * 序列所在的表名
     */
    private String tableName = DEFAULT_TABLE_NAME;

    /**
     * 存储序列名称的列名
     */
    private String nameColumnName = DEFAULT_NAME_COLUMN_NAME;

    /**
     * 存储序列值的列名
     */
    private String valueColumnName = DEFAULT_VALUE_COLUMN_NAME;

    /**
     * 存储序列最后更新时间的列名
     */
    private String gmtModifiedColumnName = DEFAULT_GMT_MODIFIED_COLUMN_NAME;

    private volatile String selectSql;
    private volatile String updateSql;

    private static void closeResultSet(ResultSet resultSet) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                log.debug("Could not close JDBC ResultSet", e);
            } catch (Throwable e) {
                log.debug("Unexpected exception on closing JDBC ResultSet", e);
            }
        }
    }

    private static void closeStatement(Statement statement) {
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                log.debug("Could not close JDBC Statement", e);
            } catch (Throwable e) {
                log.debug("Unexpected exception on closing JDBC Statement", e);
            }
        }
    }

    private static void closeConnection(Connection connection) {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                log.debug("Could not close JDBC Session", e);
            } catch (Throwable e) {
                log.debug("Unexpected exception on closing JDBC Session", e);
            }
        }
    }

    public Range nextRange(String name) throws SequenceException {
        if (name == null) {
            throw new IllegalArgumentException("序列名称不能为空");
        }
        long oldValue;
        long newValue;
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        for (int i = 0; i < retryTimes + 1; ++i) {
            try {
                connection = dataSource.getConnection();
                preparedStatement = connection.prepareStatement(getSelectSql());
                preparedStatement.setString(1, name);
                resultSet = preparedStatement.executeQuery();
                resultSet.next();
                oldValue = resultSet.getLong(1);
                if (oldValue < 0) {
                    StringBuilder sb = new StringBuilder();
                    sb.append("sequence value cannot be less than zero, value = ").append(oldValue);
                    sb.append(", please check table ").append(getTableName());
                    throw new SequenceException(sb.toString());
                }
                if (oldValue > Long.MAX_VALUE - DELTA) {
                    StringBuilder sb = new StringBuilder();
                    sb.append("sequence value overflow, value = ").append(oldValue);
                    sb.append(", please check table ").append(getTableName());
                    throw new SequenceException(sb.toString());
                }
                newValue = oldValue + getStep();
            } catch (SQLException e) {
                throw new SequenceException(e);
            } finally {
                closeResultSet(resultSet);
                resultSet = null;
                closeStatement(preparedStatement);
                preparedStatement = null;
                closeConnection(connection);
                connection = null;
            }
            try {
                connection = dataSource.getConnection();
                preparedStatement = connection.prepareStatement(getUpdateSql());
                preparedStatement.setLong(1, newValue);
                preparedStatement.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
                preparedStatement.setString(3, name);
                preparedStatement.setLong(4, oldValue);
                int affectedRows = preparedStatement.executeUpdate();
                if (affectedRows == 0) {
                    continue;
                }
                return new Range(oldValue + 1, newValue);
            } catch (SQLException e) {
                throw new SequenceException(e);
            } finally {
                closeStatement(preparedStatement);
                preparedStatement = null;
                closeConnection(connection);
                connection = null;
            }
        }
        throw new SequenceException("executor too many times, retryTimes = " + retryTimes);
    }

    private String getSelectSql() {
        if (selectSql == null) {
            synchronized (this) {
                if (selectSql == null) {
                    StringBuilder sb = new StringBuilder();
                    sb.append("query ").append(getValueColumnName());
                    sb.append(" from ").append(getTableName());
                    sb.append(" where ").append(getNameColumnName()).append(" = ?");
                    selectSql = sb.toString();
                }
            }
        }

        return selectSql;
    }

    private String getUpdateSql() {
        if (updateSql == null) {
            synchronized (this) {
                if (updateSql == null) {
                    StringBuilder sb = new StringBuilder();
                    sb.append("update ").append(getTableName());
                    sb.append(" set ").append(getValueColumnName()).append(" = ?, ");
                    sb.append(getGmtModifiedColumnName()).append(" = ? where ");
                    sb.append(getNameColumnName()).append(" = ? and ");
                    sb.append(getValueColumnName()).append(" = ?");
                    updateSql = sb.toString();
                }
            }
        }
        return updateSql;
    }

    public void setRetryTimes(int retryTimes) {
        if (retryTimes < 0) {
            throw new IllegalArgumentException("Property retryTimes cannot be less than zero, retryTimes = " + retryTimes);
        }
        this.retryTimes = retryTimes;
    }

    public int getStep() {
        return step;
    }

    public void setStep(int step) {
        if (step < MIN_STEP || step > MAX_STEP) {
            StringBuilder sb = new StringBuilder();
            sb.append("Property step out of range [").append(MIN_STEP);
            sb.append(",").append(MAX_STEP).append("], step = ").append(step);
            throw new IllegalArgumentException(sb.toString());
        }
        this.step = step;
    }

    public Logger log() {
        return log;
    }
}
